﻿// Copyright (c) 2009, James Killick
// http://code.google.com/p/kosher/
// All rights reserved.

// Redistribution and use in source and binary forms, with or without 
// modification, are permitted provided that the following conditions are met:

//     * Redistributions of source code must retain the above copyright notice, 
//       this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright notice,
//       this list of conditions and the following disclaimer in the documentation
//       and/or other materials provided with the distribution.
//     * Neither the name of the Kosher team nor the names of its contributors may 
//       be used to endorse or promote products derived from this software without 
//       specific prior written permission.

// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Linq.Expressions;

namespace Kosher
{
    public abstract class Validator<T, TError> : IValidator<T, TError>
    {
        private readonly List<ITestableTarget<T, TError>> _targets = new List<ITestableTarget<T, TError>>();
        private readonly KeyedErrorCollection<TError> _errors = new KeyedErrorCollection<TError>();

        private void GetErrorsFromValidationTarget(ITestableTarget<T, TError> target)
        {
            if (target.Errors.Count > 0)
            {
                bool alreadyExists = (_errors.Where(c => c.Key == target.Key).Count() > 0);
                var errors = alreadyExists ? _errors[target.Key] : new ErrorCollection<TError>(target.Key);

                if (!alreadyExists)
                {
                    _errors.Add(errors);
                }
                target.Errors.ForEach(e => errors.Add(e));
            }
        }

        /// <summary>
        /// Specifies what should be tested in subsequent calls to the IsTrue method.
        /// </summary>
        /// <param name="func">A Func that defines what should be tested, e.g. obj => obj.MyProperty will test the 'MyProperty' of 'obj'</param>
        /// <param name="key">Specifies what key any errors should be index against in the errors collection. Keys must be unique</param>
        /// <returns>The ICondition interface that allows you to specify tests to apply to the test target</returns>
        protected ICondition<TResult, TError> AssertFor<TResult>(Func<T, TResult> func, string key)
        {
            Guard.ArgumentNotNull(func, "func");
            Guard.ArgumentNotNullOrEmpty(key, "key");
            Guard.IsTrue(
                () => _targets.Where(t => t.Key == key).Count() == 0,
                () => { throw new ValidationException(string.Format(CultureInfo.CurrentUICulture, "The key '{0}' already exists", key)); });

            var builder = new TestBuilder<T, TResult, TError>(func, key);
            _targets.Add(builder.Target);

            return builder;
        }

        /// <summary>
        /// Specifies what should be tested in subsequent calls to the IsTrue method.
        /// The key that errors are indexed under is inferred from the expression (only member expressions are supported).
        /// </summary>
        /// <param name="expression">An expression that defines what should be tested, e.g. obj => obj.MyProperty will test the 'MyProperty' of 'obj'</param>
        /// <returns>The ICondition interface that allows you to specify tests to apply to the test target</returns>
        protected ICondition<TResult, TError> AssertFor<TResult>(Expression<Func<T, TResult>> expression)
        {
            Guard.ArgumentNotNull(expression, "expression");

            if (expression.Body.NodeType != ExpressionType.MemberAccess)
            {
                throw new NotSupportedException("AssertFor only supports member expressions");
            }

            string key = ((MemberExpression)expression.Body).Member.Name;
            return AssertFor(expression.Compile(), key);
        }

        #region IValidator<T> Members

        /// <summary>
        /// Performs validation on the passed value.
        /// </summary>
        /// <param name="value">The value to validate</param>
        /// <returns>IsTrue if the value passed validation, false otherwise</returns>
        public virtual bool IsValid(T value)
        {
            Errors.Clear();

            bool passed = true;

            foreach (var target in _targets)
            {
                try
                {
                    passed &= target.IsValid(value);
                }
                catch (AbortValidationException)
                {
                    return false;
                }
                finally
                {
                    GetErrorsFromValidationTarget(target);
                }
            }

            return passed;
        }

        public KeyedErrorCollection<TError> Errors
        {
            get { return _errors; }
        }

        #endregion
    }
}
